Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Non-kerchunk backend for HDF5/netcdf4 files. #87

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from

Conversation

sharkinsspatial
Copy link
Collaborator

@sharkinsspatial sharkinsspatial commented Apr 22, 2024

This is a rudimentary initial implementation for #78. The core code is ported directly from kerchunk's hdf backend. I have not ported the bulk of the kerchunk backend's specialized encoding translation logic but I'll try to do so incrementally so that we can build complete test coverage for the many edge cases it currently covers.

@sharkinsspatial sharkinsspatial marked this pull request as draft April 22, 2024 18:37
Copy link
Collaborator

@TomNicholas TomNicholas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great so far @sharkinsspatial !

kerchunk backend's specialized encoding translation logic

This part I would really like to either factor out, or at a least really understand what it's doing. See #68

@@ -0,0 +1,206 @@
from typing import List, Mapping, Optional

import fsspec
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does one need fsspec if reading a local file? Is there any other way to read from S3 without fsspec at all?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not with a filesystem-like API. You would have to use boto3 or aiobotocore directly.

This is one of the great virtues of fsspec and is not to be under-valued.

Comment on lines 188 to 191
def virtual_vars_from_hdf(
path: str,
drop_variables: Optional[List[str]] = None,
) -> Mapping[str, xr.Variable]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this an a way to interface with the code in open_virtual_dataset

@rabernat
Copy link
Collaborator

This looks cool @sharkinsspatial!

My opinion is that it doesn't make sense to just forklift the kerchunk code into virtualizarr. What I would love to see is an extremely tight, strictly typed, unit-tested total refactor of the parsing logic. I think you're headed down the right path, but I encourage you to push as far as you can in that direction.

@TomNicholas TomNicholas added enhancement New feature or request references generation Reading byte ranges from archival files labels Apr 22, 2024
@sharkinsspatial
Copy link
Collaborator Author

@rabernat Fully agree with your take above 👆 👍 . I'm trying to work through this incrementally whenever I can find some spare time. In the spirit of thorough test coverage 🎊 looking through your issue pydata/xarray#7388 and the corresponding PR I'm not sure what the proper incantation of variable encoding configuration is to use blosc with the netcdf4 engine? Do you have an example of this that you can provide?

@TomNicholas TomNicholas mentioned this pull request May 14, 2024
6 tasks
if mapping["scale_factor"] != 1 or mapping["add_offset"] != 0:
float_dtype = _choose_float_dtype(dtype=dataset.dtype, mapping=mapping)
target_dtype = np.dtype(float_dtype)
codec = FixedScaleOffset(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you able to make this test parametrization pass with this PR? It's currently xfailed because open_virtual_dataset doesn't know how to handle scale factor encoding.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misunderstanding, but none of the hdf reader code will be called for loadable_variables and this block would only be entered for a loaded variable. Is that correct?


shape = tuple(math.ceil(a / b) for a, b in zip(dataset.shape, dataset.chunks))
paths = np.empty(shape, dtype=np.dtypes.StringDType) # type: ignore
offsets = np.empty(shape, dtype=np.int32)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #177 , these arrays will need to be uint64 instead of int32.

manifest = _dataset_chunk_manifest(path, dataset)
if manifest:
chunks = dataset.chunks if dataset.chunks else dataset.shape
codecs = codecs_from_dataset(dataset)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving compressor=None causes ambiguity for roundtripping v3 metadata (ZArray -> disk -> ZArray) because we can't determine if it's a list of 2 filters or a list of one filter and one compressor. zlib is a compression codec and FixedScaleOffset is not, but should they both be treated as filters?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ghidalgo3 My rationale for describing the full codec chain in the filters property was the fact that internally HDF5 does not distinguish compressors and filters, the entire encoding chain is represented as filters. Since we don't need to worry about v2 interoperability, I think we can just focus with aligning with v3's api (which still seem to be in a state of flux). I think I prefer the approach proposed in zarr-developers/zarr-python#1944 (comment) but I don't know where that leaves me in the interim until a final decision gets made on the v3 api path 🤔. For v3 compatibility we'll also need to track zarr-developers/numcodecs#524 so we use numcodecs which are compatible with the new v3 codec specification. TLDR I think we might be in flux for some time while upstream v3 decisions get made.

@sharkinsspatial
Copy link
Collaborator Author

sharkinsspatial commented Jul 24, 2024

@ghidalgo3 I also want to address your question from your PR #193

Also, what happens if a source file uses a codec that is not one of the specified codecs of ZarrV3? Does that mean the file cannot be represented in ZarrV3? Seems rather onerous.

IIUC different v3 implementations will support a codec registry zarr-developers/zarr-python#1588 to make the codec support fully extensible. Codec discovery and registration has always been a thorny problem (this is a big issue in the HDF space) but I'm hopeful that this approach will be flexible.

@sharkinsspatial
Copy link
Collaborator Author

@TomAugspurger I'm trying to merge main into my branch so I can also investigate removing the Pydantic dependency in this branch and have my pre-commit working correctly again. But I seem to be hitting an issue with some change in zarr.py from #213. All of the roundtrip tests are failing on to_kerchunk_json with TypeError: array([xxx]) is not JSON serializable but I can't seem to find which of your changes is causing this 🤔 and I'm a bit stumped. I'm packing and moving countries this week 🇲🇽 -> 🇨🇦 so I'm probably missing something obvious 😄 . Any thoughts?

@TomAugspurger
Copy link
Contributor

@sharkinsspatial this is a behavior change in ZArray.dict during the move away from pydantic.

Something like this seems to fix the failing tests

diff --git a/virtualizarr/zarr.py b/virtualizarr/zarr.py
index 824892c..87bb453 100644
--- a/virtualizarr/zarr.py
+++ b/virtualizarr/zarr.py
@@ -106,8 +106,15 @@ class ZArray:
 
     def to_kerchunk_json(self) -> str:
         zarray_dict = self.dict()
-        if zarray_dict["fill_value"] is np.nan:
+
+        fill_value = zarray_dict["fill_value"]
+
+        if fill_value is np.nan:
             zarray_dict["fill_value"] = None
+
+        elif isinstance(fill_value, (np.number, np.ndarray)):
+            zarray_dict["fill_value"] = fill_value.item()
+
         return ujson.dumps(zarray_dict)
 
     # ZArray.dict seems to shadow "dict", so we need the type ignore in

I'm not sure what behavior we want dict() to have. Whether it should faithfully return the value set, or whether it should attempt to cast to something else.

@TomNicholas
Copy link
Collaborator

TomNicholas commented Aug 10, 2024

The ZArray class was supposed to be a way to standardize the metadata, allowing the rest of the package to not worry about any differences that kerchunk throws at us.

The .dict() method we just got for free via inheriting from the pydantic base model.

I think we should migrate the interface of ZArray towards providing a unified representation of the metadata, and also try to move its API closer to that of zarr-python's ZMetaData class because really we want to be using that instead.

(Not sure if that actually answers your question)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
encoding enhancement New feature or request references generation Reading byte ranges from archival files
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants